namespace Effector {
    public class GeglContext : GLib.Object {
        public Gegl.Node context {get; private set;}
        private static GeglContext? instance = null;

        private GeglContext() {
            this.context = new Gegl.Node();
        }

        public static Gegl.Node s() {
            if (GeglContext.instance == null) {
                GeglContext.instance = new GeglContext();
            }
            return GeglContext.instance.context;
        }
    }

    public class PadSink : GFlow.SimpleSink {
        public Node enode {get; private set;}
        public string padname {get; private set;}
        public PadSink (GLib.Type t, Node n, string padname) {
            base(t);
            this.enode = n;
            this.padname = padname;
        }

        public void cb_unlinked_sink(GFlow.Dock target) {
            this.enode.gegl.disconnect(this.padname);
        }
    }

    public class PadSource : GFlow.SimpleSource {
        public Node enode {get; private set;}
        public string padname {get; private set;}
        public PadSource (GLib.Type t, Node n, string padname) {
            base(t);
            this.enode = n;
            this.padname = padname;
        }

        public void cb_linked_source(GFlow.Dock target) {
            var tgt = (target as PadSink);
            this.enode.gegl.connect_to(this.padname, tgt.enode.gegl, tgt.padname);

        }
    }

    private struct DoubleSpecs {
        public double minimum;
        public double maximum;
        public double default_value;
        public double ui_minimum;
        public double ui_maximum;
        public double ui_gamma;
        public double big_step_size;
        public double small_step_size;
        public int ui_digits;
    }

    private struct IntSpecs {
        public int minimum;
        public int maximum;
        public int default_value;
        public int ui_minimum;
        public int ui_maximum;
        public double ui_gamma;
        public int big_step_size;
        public int small_step_size;
    }

    public class Node : GFlow.SimpleNode {
        public Gegl.Node gegl {get; private set;}
        public Gtk.Grid grid;

        // TODO: remove as soon as the following commit has landed
        //       in an official vala-release. then set the minimum version
        //       for the compiler to that release
        //       Also remove the DoubleSpecs struct
        //       https://github.com/GNOME/vala/commit/73377c492c288f63887ba801293ed1db66251dd4
        private DoubleSpecs extract_specs_double(GLib.ParamSpec p) {
            var p_base = (char*)p;
            var ds = DoubleSpecs();
            GLib.Memory.copy(&ds.minimum,         p_base +  72, 8);
            GLib.Memory.copy(&ds.maximum,         p_base +  80, 8);
            GLib.Memory.copy(&ds.default_value,   p_base +  88, 8);
            GLib.Memory.copy(&ds.ui_minimum,      p_base + 104, 8);
            GLib.Memory.copy(&ds.ui_maximum,      p_base + 112, 8);
            GLib.Memory.copy(&ds.ui_gamma,        p_base + 120, 8);
            GLib.Memory.copy(&ds.small_step_size, p_base + 128, 8);
            GLib.Memory.copy(&ds.big_step_size,   p_base + 136, 8);
            GLib.Memory.copy(&ds.ui_digits,       p_base + 144, 8);
            return ds;
        }

        // TODO: remove as soon as the following commit has landed
        //       in an official vala-release. then set the minimum version
        //       for the compiler to that release
        //       Also remove the IntSpecs struct
        //       https://github.com/GNOME/vala/commit/73377c492c288f63887ba801293ed1db66251dd4
        private IntSpecs extract_specs_int(GLib.ParamSpec p) {
            var p_base = (char*)p;
            var ds = IntSpecs();
            GLib.Memory.copy(&ds.minimum,         p_base +  72, 4);
            GLib.Memory.copy(&ds.maximum,         p_base +  76, 4);
            GLib.Memory.copy(&ds.default_value,   p_base +  84, 4);
            GLib.Memory.copy(&ds.ui_minimum,      p_base +  92, 4);
            GLib.Memory.copy(&ds.ui_maximum,      p_base +  88, 4);
            GLib.Memory.copy(&ds.ui_gamma,        p_base +  96, 8);
            GLib.Memory.copy(&ds.small_step_size, p_base + 108, 4);
            GLib.Memory.copy(&ds.big_step_size,   p_base + 104, 4);
            return ds;
        }

        public Node(string gegl_id) {
            var context = GeglContext.s();
            gegl = context.create_child(gegl_id);
            this.name = gegl_id;
            this.grid = new Gtk.Grid();
            this.grid.column_spacing = 5;
            this.grid.row_spacing = 5;
            this.grid.vexpand = this.grid.vexpand_set = true;

            var properties = Gegl.Operation.list_properties(gegl_id);
            int grid_y_count = 0;
            foreach(ParamSpec p in properties) {
                Gtk.Widget widget = new Gtk.Button.with_label(p.get_name());
                if (p.value_type == typeof(double)) {
                    var specs = this.extract_specs_double(p);
                    var adjustment = new Gtk.Adjustment(specs.default_value, specs.minimum, specs.maximum, specs.big_step_size, 0, 0);
                    widget = new Gtk.SpinButton(adjustment, specs.small_step_size, specs.ui_digits);
                    adjustment.value_changed.connect(()=>{
                        this.gegl.set_property(p.get_name(), adjustment.value);
                    });
                } else if (p.value_type == typeof(string)) {
                    if (p.get_name() == "font") {
                        widget = new Gtk.FontButton();
                        (widget as Gtk.FontButton).font_set.connect(()=>{
                            this.gegl.set_property(p.get_name(), (widget as Gtk.FontButton).font_name);
                        });
                    } else if (p.get_name() == "path") {
                        widget = new Gtk.FileChooserButton("Open Image", Gtk.FileChooserAction.OPEN);
                        if (this.gegl.list_output_pads().length == 0) {
                            widget = new Gtk.FileChooserWidget(Gtk.FileChooserAction.SAVE);
                        }
                        (widget as Gtk.FileChooser).selection_changed.connect(()=>{
                            foreach (unowned string uri in (widget as Gtk.FileChooser).get_uris()) {
                                message(uri);
                                if (uri.has_prefix("file://")) {
                                    this.gegl.set_property(p.get_name(), uri.replace("file://",""));
                                }
                                this.gegl.set_property("uri", uri);
                            }
                        });
                    } else {
                        widget = new Gtk.Entry();
                        (widget as Gtk.Editable).changed.connect(()=>{
                            this.gegl.set_property(p.get_name(), (widget as Gtk.Entry).text);
                        });
                    }
                } else if (p.value_type == typeof(Gegl.Color)) {
                    widget = new Gtk.ColorButton();
                    (widget as Gtk.ColorButton).color_set.connect(()=>{
                        var rgba = (widget as Gtk.ColorButton).rgba;
                        double r, g, b;
                        r = rgba.red;
                        g = rgba.green;
                        b = rgba.blue;
                        this.gegl.set_property(p.get_name(), new Gegl.Color("rgb(%f,%f,%f)".printf(r,g,b)));
                    });
                } else if (p.value_type == typeof(bool)) {
                    widget = new Gtk.Switch();
                    (widget as Gtk.Switch).notify["active"].connect(()=>{
                        this.gegl.set_property(p.get_name(), (widget as Gtk.Switch).active);
                    });
                } else if (p.value_type == typeof(int)) {
                    var specs = this.extract_specs_int(p);
                    var adjustment = new Gtk.Adjustment(specs.default_value, specs.minimum, specs.maximum, specs.small_step_size, 0, 0);
                    widget = new Gtk.SpinButton(adjustment, specs.small_step_size, 0);
                    adjustment.value_changed.connect(()=>{
                        this.gegl.set_property(p.get_name(), (int)adjustment.value);
                    });
                } else if (p is ParamSpecEnum) {
                    var pse = (ParamSpecEnum)p;
                    var liststore = new Gtk.ListStore(2, typeof(string), typeof(int));
                    Gtk.TreeIter iter;
                    unowned EnumClass enumc = (EnumClass) pse.enum_class;
                    for (int i = enumc.minimum; i <= enumc.maximum; i++) {
                        EnumValue? v = enumc.get_value(i);
                        if (v == null) continue;
                        liststore.append(out iter);
                        liststore.set(iter, 0, v.value_name, 1, i);
                    }
                    var combobox = new Gtk.ComboBox.with_model(liststore);
                    var renderer = new Gtk.CellRendererText ();
                    combobox.pack_start (renderer, true);
                    combobox.add_attribute (renderer, "text", 0);
                    combobox.active = 0;
                    combobox.changed.connect(()=>{
                        Value v;
                        Gtk.TreeIter readiter;
                        combobox.get_active_iter(out readiter);
                        combobox.get_model().get_value(readiter, 1, out v);
                        message(v.get_int().to_string());
                        this.gegl.set_property(p.get_name(), v.get_int());
                    });
                    widget = combobox;
                }
                this.grid.attach(new Gtk.Label(p.get_name()), 0, grid_y_count, 1, 1);
                this.grid.attach(widget, 1, grid_y_count++, 1, 1);
                message(p.get_name());
            }

            foreach (string padname in this.gegl.list_input_pads()) {
                var sink = new PadSink(typeof(int), this, padname);
                sink.name = padname;
                sink.unlinked.connect(sink.cb_unlinked_sink);
                try {
                    this.add_sink(sink);
                } catch (GFlow.NodeError e) {
                    warning("Could not create pad '%s'", padname);
                }
            }

            foreach (string padname in this.gegl.list_output_pads()) {
                var source = new PadSource(typeof(int), this, padname);
                source.name = padname;
                source.linked.connect(source.cb_linked_source);
                try {
                    this.add_source(source);
                } catch (GFlow.NodeError e) {
                    warning("Could not create pad '%s'", padname);
                }
            }

            if (this.gegl.list_output_pads().length == 0) {
                var button = new Gtk.Button.with_label("Process");
                this.grid.attach(button, 0, grid_y_count, 2, 1);
                button.clicked.connect(()=>{
                    this.gegl.process();
                });
            }
        }

    }
}
